package com.ruoyi.framework.sharding;

import com.google.common.collect.Range;
import org.apache.shardingsphere.sharding.api.sharding.ShardingAutoTableAlgorithm;
import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.RangeShardingValue;
import org.apache.shardingsphere.sharding.api.sharding.standard.StandardShardingAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * @Description: 分片算法，按年分片
 * @Author WangHan
 * @Date 2024/9/19 15:11
 * @Version 1.0
 */
public class TimeShardingAlgorithm implements StandardShardingAlgorithm<Date>, ShardingAutoTableAlgorithm {

    private static final Logger log = LoggerFactory.getLogger(TimeShardingAlgorithm.class);

    /**
     * 分片时间格式
     */
    private static final DateTimeFormatter TABLE_SHARD_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy");

    /**
     * 完整时间格式
     */
    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd HH:mm:ss");
    private static final DateTimeFormatter YYYY_MM_DD_HH_MM_SS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    /**
     * 表分片符号，例：mall_member_2024 中，分片符号为 "_"
     */
    private final String TABLE_SPLIT_SYMBOL = "_";

    private Properties props;

    private int autoTablesAmount;


    /**
     * 精准分片（新增）
     * @param tableNames 对应分片库中所有分片表的集合
     * @param preciseShardingValue 分片键值，其中 logicTableName 为逻辑表，columnName 分片键，value 为从 SQL 中解析出来的分片键的值
     * @return 表名
     */
    @Override
    public String doSharding(Collection<String> tableNames, PreciseShardingValue<Date> preciseShardingValue) {
        String logicTableName = preciseShardingValue.getLogicTableName();
        // 打印分片信息
        log.info("精确分片，节点配置表名：{}", tableNames);
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date = preciseShardingValue.getValue();

        String string = format.format(date);
        LocalDateTime dateTime = LocalDateTime.parse(string, YYYY_MM_DD_HH_MM_SS);
        String resultTableName = logicTableName + "_" + dateTime.format(TABLE_SHARD_TIME_FORMATTER);

        // 检查是否需要初始化
        if (tableNames.size() == 1) {
            // 如果只有一个表，说明需要获取所有表名
            List<String> allTableNameBySchema = ShardingAlgorithmTool.getAllTableNameBySchema(logicTableName);
            tableNames.clear();// 先清除，再加载所有的分片表
            tableNames.addAll(allTableNameBySchema);
            autoTablesAmount = allTableNameBySchema.size();
        }

        return getShardingTableAndCreate(logicTableName, resultTableName, tableNames);
    }

    /**
     * 范围分片（查询、更新、删除）
     * @param tableNames 对应分片库中所有分片表的集合
     * @param rangeShardingValue 分片范围
     * @return 表名集合
     */
    private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    @Override
    public Collection<String> doSharding(Collection<String> tableNames, RangeShardingValue<Date> rangeShardingValue) {
        String logicTableName = rangeShardingValue.getLogicTableName();
        // 打印分片信息
        log.info(">>>>>>>>>> 【INFO】范围分片，节点配置表名：{}", tableNames);
        // 检查是否需要初始化
        if (tableNames.size() == 1) {
            // 如果只有一个表，说明需要获取所有表名
            List<String> allTableNameBySchema = ShardingAlgorithmTool.getAllTableNameBySchema(logicTableName);
            tableNames.clear();// 先清除，再加载所有的分片表
            tableNames.addAll(allTableNameBySchema);
            autoTablesAmount = allTableNameBySchema.size();
        }
        // between and 的起始值
        Range<Date> valueRange = rangeShardingValue.getValueRange();
        boolean hasLowerBound = valueRange.hasLowerBound();
        boolean hasUpperBound = valueRange.hasUpperBound();
        // 获取日期范围的上下界
        Date lowerDate = null;
        Date upperDate = null;
        try {
            lowerDate = DATE_FORMATTER.parse(String.valueOf(rangeShardingValue.getValueRange().lowerEndpoint()));
            upperDate = DATE_FORMATTER.parse(String.valueOf(rangeShardingValue.getValueRange().upperEndpoint()));
        } catch (ParseException e) {
            try {
                lowerDate = DATE_FORMATTER.parse(String.valueOf(rangeShardingValue.getValueRange().lowerEndpoint()) + " 00:00:00");
                upperDate = DATE_FORMATTER.parse(String.valueOf(rangeShardingValue.getValueRange().upperEndpoint()) + " 23:59:59");
            } catch (ParseException ex) {
                throw new RuntimeException(ex);
            }
        }
        LocalDateTime lowerEndpoint = lowerDate.toInstant()
                .atZone(ZoneId.systemDefault())
                .toLocalDateTime();

        LocalDateTime upperEndpoint = upperDate.toInstant()
                .atZone(ZoneId.systemDefault())
                .toLocalDateTime();

        // 获取最大值和最小值
        LocalDateTime min = hasLowerBound ? lowerEndpoint :getLowerEndpoint(tableNames);
        LocalDateTime max = hasUpperBound ? upperEndpoint :getUpperEndpoint(tableNames);

        // 循环计算分表范围
        Set<String> resultTableNames = new LinkedHashSet<>();
        resultTableNames.add(logicTableName);
        while (min.isBefore(max) || min.equals(max)) {
            String tableName = logicTableName + TABLE_SPLIT_SYMBOL + min.format(TABLE_SHARD_TIME_FORMATTER);
            resultTableNames.add(tableName);
            min = min.plusMinutes(1);
        }
        // 查询与修改不需要执行创建表
        return resultTableNames;
    }

    @Override
    public void init(Properties props) {
        this.props = props;
    }

    @Override
    public int getAutoTablesAmount() {
        return autoTablesAmount;
    }

    @Override
    public String getType() {
        return "CLASS_BASED";
    }

    public Properties getProps() {
        return props;
    }

    /**
     * 检查分表获取的表名是否存在，不存在则自动建表
     *
     * @param logicTableName        逻辑表
     * @param resultTableNames     真实表名，例：mall_order_2022
     * @param availableTargetNames 可用的数据库表名
     * @return 存在于数据库中的真实表名集合
     */
    public Set<String> getShardingTablesAndCreate(String logicTableName, Collection<String> resultTableNames, Collection<String> availableTargetNames) {
        return resultTableNames.stream().map(o -> getShardingTableAndCreate(logicTableName, o, availableTargetNames)).collect(Collectors.toSet());
    }

    /**
     * 检查分表获取的表名是否存在，不存在则自动建表
     * @param logicTableName   逻辑表
     * @param resultTableName 真实表名，例：mall_order_2023
     * @return 确认存在于数据库中的真实表名
     */
    private String getShardingTableAndCreate(String logicTableName, String resultTableName, Collection<String> availableTargetNames) {
        // 缓存中有此表则返回，没有则判断创建
        if (availableTargetNames.contains(resultTableName)) {
            return resultTableName;
        } else {
            // 检查分表获取的表名不存在，需要自动建表
            boolean isSuccess = ShardingAlgorithmTool.createShardingTable(logicTableName, resultTableName);
            if (isSuccess) {
                // 如果建表成功，需要更新缓存
                availableTargetNames.add(resultTableName);
                autoTablesAmount++;
                return resultTableName;
            } else {
                // 如果建表失败，返回逻辑空表
                return logicTableName;
            }
        }
    }

    /**
     * 获取 最小分片值
     * @param tableNames 表名集合
     * @return 最小分片值
     */
    private LocalDateTime getLowerEndpoint(Collection<String> tableNames) {
        Optional<LocalDateTime> optional = tableNames.stream()
                .map(o -> LocalDateTime.parse(o.replace(TABLE_SPLIT_SYMBOL, "") + "01-01-01 00:00:00", DATE_TIME_FORMATTER))
                .min(Comparator.comparing(Function.identity()));
        if (optional.isPresent()) {
            return optional.get();
        } else {
            log.error(">>>>>>>>>> 【ERROR】获取数据最小分表失败，请稍后重试，tableName：{}", tableNames);
            throw new IllegalArgumentException("获取数据最小分表失败，请稍后重试");
        }
    }

    /**
     * 获取 最大分片值
     * @param tableNames 表名集合
     * @return 最大分片值
     */
    private LocalDateTime getUpperEndpoint(Collection<String> tableNames) {
        Optional<LocalDateTime> optional = tableNames.stream()
                .map(o -> LocalDateTime.parse(o.replace(TABLE_SPLIT_SYMBOL, "") + "01-01-01 00:00:00", DATE_TIME_FORMATTER))
                .max(Comparator.comparing(Function.identity()));
        if (optional.isPresent()) {
            return optional.get();
        } else {
            log.error(">>>>>>>>>> 【ERROR】获取数据最大分表失败，请稍后重试，tableName：{}", tableNames);
            throw new IllegalArgumentException("获取数据最大分表失败，请稍后重试");
        }
    }

}
